Projeto: Bem-te-vi
Autor: Diego Abreu
Arquivo: passo_a_passo_bem-te-vi.ipynb
Resumo: Este arquivo tem por objetivo detalhar cada etapa do projeto Bem-te-vi.
Explicando cada uma das decisões tomadas e o código em linguagem Python utilizado durante os processos.
Etapas:
Temos abaixo uma representação visual de como funciona o bem-te-vi:

Este projeto necessita que dos pacotes e bibliotecas abaixo:
Tweepy: Biblioteca Open-source de fácil uso para acessar a API do Twitter. Disponível em: https://tweepy.readthedocs.io/en/latest/
JSON: Pacote nativo da linguagem python para manipulação de dados em formato JSON.
Unidecode: Pacote para retirada de caracteres especiais do texto, como emojis e acentos. Disponível em: https://pypi.org/project/Unidecode/#description
Its dangerous: Pacote para criptografia de dados. Disponível em: https://pythonhosted.org/itsdangerous/
Pandas: Pacote para manipulação de dados em formato dataframe. Disponível em: https://www.anaconda.com/distribution/
Plotly: Framework para criação de gráficos e dashboard. Disponível em: https://plot.ly/
MongoDB: Para armazenar os tweets coletados usaremos o mongoDB. Disponível em:https://www.mongodb.com/
PyMongo: Pacote para conexão com o mongoDB. Disponível em: https://pypi.org/project/pymongo/
Time: Pacote nativo da linguagem python para manipulação de dados em formato data/hora.
Date time: Biblioteca para manipulação de dados em formato data/hora. Disponível em: https://pypi.org/project/DateTime/
Scikit-learn: Biblioteca de aprendizado de máquina de código aberto para a linguagem de programação Python.
NLTK: Conjunto de bibliotecas para processamento de linguagem natural simbólica.
Emoji: Pacote para manipulação de emojis.
Vader: Ferramenta de análise de sentimentos baseada regras de vocabulário.
NBConvert: Pacote para exportar o Notebook em HTML.
# Instalações:
# !pip install tweepy
# !pip install Unidecode
# !pip install itsdangerous
# !pip install DateTime
# !pip install pymongo
# !pip install emoji
# !pip install vaderSentiment
# !pip install plotly==4.1.0
# nltk.download('stopwords')
# !pip install dash==1.3.0
# !pip install dash-daq==0.2.1
# !pip install selenium
# !pip install nbconvert
# Caso não possua um desses pacotes, descomente e rode essa célula.
# Importações:
# Tweepy:
import tweepy
from tweepy.streaming import StreamListener
from tweepy import OAuthHandler
from tweepy import Stream
# JSON:
import json
# Unidecode:
from unidecode import unidecode
# Its dangerous:
from itsdangerous import URLSafeSerializer
# Pandas
import pandas as pd
# Plotly
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# PyMongo
from pymongo import MongoClient
# Time
import time
# Date Time
import datetime
# Sklearn
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn import metrics
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
from sklearn.feature_extraction.text import CountVectorizer
# NLTK
import nltk
from nltk.corpus import stopwords
# Emoji
import emoji
# Vader
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
# NBConvert
import nbconvert
Para a coleta de dados no twitter é necessário ter uma conta de desenvolvedor na rede social. Após isso é necessário submeter uma aplicação para conseguir as 4 chaves de autenticação. A conta e as chaves devem ser obtidas pelo site: https://developer.twitter.com/ As 4 chaves são:
# Chaves da API do Twitter:
# Consumer Key
consumer_key = "INSIRA_AQUI_SUA_CHAVE_DA_API_DO_TWITTER"
# Consumer Secret
consumer_secret = "INSIRA_AQUI_SUA_CHAVE_DA_API_DO_TWITTER"
# Access Token
access_token = "INSIRA_AQUI_SUA_CHAVE_DA_API_DO_TWITTER"
# Access Token Secret
access_token_secret = "INSIRA_AQUI_SUA_CHAVE_DA_API_DO_TWITTER"
Usaremos o pacote itsdangerous para criptografar o campo com a identificação do usuário. Para essa transformação, é necessário definir uma chave que será usada como base dessa transformação. Pode ser uma simples palavra como 'panqueca' ou um termo mais complexo, fica a seu critério.
Essa mesma chave, caso necessário, poderá ser usada posteriormente para descriptografar o dado.
# Chave para criptografia:
cripto_key = 'INSIRA_AQUI_SUA_CHAVE_DE_CRIPTOGRAFIA'
É preciso definir quais são as palavras-chave queremos buscar. Tweets com essas que contenham essas palavras seram capturados pela nossa aplicação:
# Lista de palavras-chave para a coleta dos tweets:
keywords = ['INSIRA_AQUI_SUAS_PALAVRAS_CHAVE_PARA_BUSCA']
# Para mais de uma palavra faça:
# keywords = ['primeira', 'segunda','terceira', 'quarta', 'quinta']
Na sequência também é necessário que se defina qual será o nome da base de dados que receberá os dados coletas. Pode ser uma base que já exista ou um novo.
# Nome da base de dados no MongoDB:
banco = 'INSIRA_AQUI_O_NOME_DA_BASE_DE_DADOS'
Criação das funções que serão utilizadas no processo de coleta:
# Autenticação com API do twitter:
auth = OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
# Função de criptografia:
cripto = URLSafeSerializer(cripto_key)
Conforme documentação do twitter disponível em: https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/tweet-object . Definiremos na função de filtragem os campos que serão coletados. Os campos de cada tweet que coletaremos nesse projeto são:
# Função de filtragem de tweets:
class Filtratweets(StreamListener):
def on_data(self, dados):
tweet = json.loads(dados)
created_at = tweet["created_at"]
cp_screen_name = cripto.dumps(tweet["user"]["screen_name"]) # Aplicamos aqui a função cripto para que o campo já entre camuflado no banco de dados.
verified = tweet["user"]["verified"]
text = tweet["text"]
location = tweet["user"]["location"]
obj = {"created_at":created_at,
"cp_screen_name": cp_screen_name,
"verified":verified,
"text":text,
"location":location}
tweetind = col.insert_one(obj).inserted_id
print (obj)
return True
def on_error(self, status_code):
if status_code == 420:
return True
# Objeto filtragem de tweets
filtratweets = Filtratweets()
# Objeto captura de tweets, faz a conexão via API do Twitter
capturatweets = Stream(auth, listener = filtratweets, wait_on_rate_limit=True)
O próximo passo é estabelecer a conexão com o banco de dados, criar o banco e a coleção.
# Criação da conexão ao MongoDB
client = MongoClient('localhost', 27017)
# Criação do banco de dados
db = client[banco]
# Criação da collection
col = db[banco]
O Tweepy nos garante certa robustez na captura de dados, mas dependendo dos termos buscados o retorno pode trazer um alto volume de tweets em um curto período. O que pode causar erro e a interrupção da nossa aplicação, sendo necessário iniciá-la novamente. Para evitarmos isso, colocaremos a captura dentro de uma função que inicia a captura normalmente, mas que em caso de erro, pausa a coleta durante um determinado tempo (no caso, 10 segundos),e após esse período ele inicia a retoma novamente.
# Função que inicia a coleta de tweets:
def inicia_coleta():
while True:
try:
capturatweets.filter(languages=["pt"], track=keywords) # O parâmetro language define que capturaremos tweets em português
except:
time.sleep(10) # Define o tempo que a aplicação deve aguardar para rodar novavemente em caso de erros.
continue
# Inicia a coleta dos tweets
inicia_coleta()
# --> Pressione o botão Stop na barra de ferramentas duas vezes para encerrar a captura dos Tweets
# Finaliza a conexão com a API do Twitter
capturatweets.disconnect()
Nessa etapa, primeiramente, vamos acessar os dados no banco de dados e visualizá-los utilizando o pacote pandas.
# criação de um dataset com dados retornados do MongoDB
dataset = [{"created_at": item["created_at"],
"cp_screen_name": item["cp_screen_name"],
"verified": item["verified"],
"text": item["text"],
"location": item["location"],
} for item in col.find()]
# Criação do dataframe
df = pd.DataFrame(dataset)
Caso tenha feito a coleta em outro momento ou queira uar um dataset já existente. basta descomentar e rodar a célula abaixo.
O único requisito para que o código funcione bem, é que o dataset tenha as variáveis (colunas) com os mesmos nomes: created_at, cp_screen_name, verified, text e location.
# Código para aplicar a análise em outro dataset, descomente e rode.
# O arquivo da coleta tweets_GRExFLA.csv está disponível no diretório do projeto:
# df = pd.read_csv('dados_tweets_coletados/tweets_GRExFLA.csv')
Nesse arquivo de passo a passo, faremos a análise de tweets coletados durante o jogo de futebol Grêmio X Flamengo pela Copa Libertadores da América que ocorreu no dia 02/10/2019.
O objetivo era observar o comportamento da torcida rubro-negra. Por isso as palavras-chave utilizada na busca foram:
'flamengo', 'Flamengo', '#CRF', 'Mengo' e 'mengo'.
A API do Tweepy é case-sensitive, ou seja, faz diferenciação entre maiúsculas e minúsculas. Por isso, as palavras-chave forma escritas de diferentes formas.
Vamos partir agora para nossa análise:
Total de registros em nosso dataset:
total_tweets = len(df)
total_tweets
Visualização dos 5 primeiros tweets coletados:
df.head()
As células abaixo configura o ambiente para mostrar os dados e processos de forma mais agradavél.
# Ajuste de visualização:
pd.set_option('display.max_colwidth', -1)
pd.set_option('display.max_columns', 100)
pd.set_option('display.max_rows', 100)
# Não mostrar avisos e alertas sobre versões desatualizadas, processos e etc.
import sys
if not sys.warnoptions:
import warnings
warnings.simplefilter("ignore")
df.head()
Felizmente os dados já vem com um boa estrutura, praticamente pronto para iniciar a análise exploraória. Portanto, inicialmente faremos apenas o tratamento na variável de data/hora (created_at), pois ele não vem em nosso fuso horário. Os demais tratamento serão feitos a medida em que forem necessário na fase exploratória.
# Tratamento da váriavel created_at:
# Função para formatar data/hora:
def format_datetime(dt_series):
def get_split_date(strdt):
split_date = strdt.split()
str_date = split_date[1] + ' ' + split_date[2] + ' ' + split_date[5] + ' ' + split_date[3]+ ' ' + split_date[4]
return str_date
dt_series = pd.to_datetime(dt_series.apply(lambda x: get_split_date(x)), format = '%b %d %Y %H:%M:%S %z')
return dt_series
# Cria nova variável para o horário correto:
df['data_hora'] = format_datetime(df['created_at'])
# Converte para o nosso fuso horário:
df['data_hora'] = df['data_hora'].dt.tz_convert('America/Sao_Paulo')
Visualização dos 5 primeiros tweets coletados, após o tratamento de data e hora:
df.head()
Caso queira, é possível exportar o dataset no estado descomentando e rodando o código abaixo:
# Exporta a coleta em arquivo .csv , descomente e rode:
#df.to_csv('coleta_de_tweets.csv', index=False)
Para nossa análise exploratória definimos algumas questões para serem respondidas:
Nessa etapa utilizaremos o pacote pandas para as análises quantitativas, os algoritmos Multinomial Naive Bayes e Vader para a análise de sentimentos e o Plotly para a criação de gráficos.
Na etapa anterior verificamos a quantidade de registros. Esse valor é a nossa quantidade de tweets coletados. Porém podemos enriquecer essa informação identificando quais deles são originais e quais deles são retweets(tweets que foram replicados por outros usuários). Os retweets tem como característica ter a sigal "RT" antes do conteúdo. Portanto vamos identicar em quantos registros a variável "text" começa com "RT".
# Quantidade total de tweets:
total_tweets = len(df)
# Cria uma variavél chamada "retweeted" que armazena se o conteúdo do tweet começa com RT ou não:
df['retweeted'] = df['text'].str.startswith('RT')
# Contagem dos retweets:
retweets = len(df[df['retweeted'] == True])
# Contagem dos originais:
originais = len(df[df['retweeted'] == False])
# Exibindo as contagens:
print("Quantidade de tweets originais: ", originais)
print("Quantidade de retweets: ", retweets)
print("Quantidade total dos tweets: ", total_tweets)
# Gráfico da quantidade total de tweets:
total_tt_st = str(total_tweets) + " Tweets"
fig = go.Figure(data = [go.Pie(labels = ['Originais','Retweets'],values = [originais, retweets],
hole = .5, marker_colors = ['gold', 'goldenrod'],
textinfo = 'label+percent', hoverinfo = 'value')])
fig.update_layout(template="plotly_dark",
title = go.layout.Title(text = total_tt_st,xref = "paper", x=0.5))
fig.show()
Apesar de criptografados, podemos fazer a contagem do número de usuários. Pois o algoritmo de criptografia segue um padrão baseado pela palavra chave, então palavras iguais possuem correspondentes criptogrados iguais. Portanto basta apenas fazer uma contagem simples. Assim como o tópico anterior, também podemos enriquecer a análise. Pois através da variavél "verified" podemos identificar quantos usuários possuem o selo de verificação do twitter.
# Contagem de usuários únicos:
qtd_usuarios_unicos = df['cp_screen_name'].nunique()
# Contagem de usuários verificados:
usuarios_verificados = df[df['verified'] == True]
total_tt_verificados = len(usuarios_verificados)
qtd_usuarios_verificados = usuarios_verificados['cp_screen_name'].nunique()
# Contagem de usuários não verificados:
usuarios_nao_verificados = df[df['verified'] == False]
total_tt_nao_verificados = len(usuarios_nao_verificados)
qtd_usuarios_nao_verificados = usuarios_nao_verificados['cp_screen_name'].nunique()
# Exibindo as contagens:
print("Quantidade de usuários verificados: ", qtd_usuarios_verificados)
print("Quantidade de usuários não verificados: ", qtd_usuarios_nao_verificados)
print("Quantidade total de usuários: ", qtd_usuarios_unicos)
# Gráfico da quantidade total de usuários:
total_us_st = str(qtd_usuarios_unicos) + " Usuários"
fig = go.Figure(data = [go.Pie(labels = ['Verificados', 'Não verificados'],
values = [qtd_usuarios_verificados, qtd_usuarios_nao_verificados],
hole = .5, marker_colors =['darkgoldenrod','gold'],
textinfo = 'label+percent', hoverinfo = 'value')])
fig.update_layout(template="plotly_dark",
title = go.layout.Title(text = total_us_st, xref = "paper", x=0.5))
fig.show()
Para obtermos essa informação precisamos contar quantos tweets foram publicados no mesmo horário.
# Cria um dataframe com a quantidade de tweets por horário:
tw_x_pd = df['data_hora'].value_counts().to_frame().reset_index()
tw_x_pd.columns = ['data_hora', 'qtd_tweets']
# Ordenar pelo horário:
tw_x_pd = tw_x_pd.sort_values(by=['data_hora'])
# Exibe os 5 primeiros horários:
tw_x_pd.head()
# Gráfico da quantidade ao longo do período:
fig = go.Figure(data = [go.Scatter(x = tw_x_pd['data_hora'], y = tw_x_pd['qtd_tweets'],
fill = 'tozeroy', mode = 'lines', line = dict(color = 'gold', width = 0.5))])
fig.update_layout(template = "plotly_dark",
title = go.layout.Title(text = "Tweets ao longo do período",xref = "paper", x = 0.5))
fig.show()
Do nosso projeto essa talvez seja a com maior grau de complexidade. Para a análise de sentimentos usaremos dois algoritmos o Multinomial Naive Bayes e Vader.
Há diversos algoritmos de análise de sentimentos disponíveis com bom desempenho, o Vader é um exemplo, porém eles não fazem essa análise em língua portuguesa. Devido a grande quantidade de tweets em algumas análises, a tradução do tweets para inglês para depois passarem por um desses algoritmos, se mostrou demorado e em alguns casos apresentou erros na API de tradução devido ao alto número de traduções.
Sendo descartada a opção de utilizar um algoritmo pronto, a alternativa para nosso projeto foi criar um algoritmo utilizando Multinomial NB treinado com um dataset obtido no Kaggle. Link para o dataset:https://www.kaggle.com/augustop/portuguese-tweets-for-sentiment-analysis/ .
Os arquivos usados foram os Test3classes.csv e Train3Classes.csv. O dataset de treino possui 100 mil tweets em português já classificados em Negativo, Positivo e Neutro. O de teste possui 4999 registros classificados.
Para facilitar nosso processo, baseado nesses dois arquivos foram criados os arquivos df_treino.csv e df_teste.csv. Eles teem o mesmo conteúdo dos arquivos do Kaggle porém com alguns ajustes: exclusão de colunas desnecessárias, e transformação dos valores da variável "sentiment" de 0,1,2 para Negativo, Positivo e Neutro. Esses arquivos estão disponíveis no diretório desse projeto.
# Criação do Algoritmo de análise de sentimentos:
# Leitura dos dados de treino:
df_treino = pd.read_csv("dados_base_analise_de_sentimentos/df_treino.csv")
# Importação de stop words em português do pacote NLTK:
portugues_stops = set(stopwords.words('portuguese'))
# Adição de novas stop words identificadas como faltantes em análises anteriores:
novas_stopwords = ['tá', 'ta', 'pra','pro']
portugues_stops = portugues_stops.union(novas_stopwords)
# Função de tratamento do texto:
tf_vectorizer = TfidfVectorizer(stop_words = portugues_stops,analyzer='word', ngram_range=(1, 1),
lowercase=True, use_idf=True, strip_accents='unicode')
# Definição de variáveis de treinamento e aplicação da função de tratamento de texto:
treino_x = tf_vectorizer.fit_transform(df_treino['tweets'])
treino_y = df_treino['sentimento']
# Criação do modelo de análise:
classificador_MNB = MultinomialNB()
# Treinamento do modelo:
classificador_MNB.fit(treino_x, treino_y)
Teste do modelo:
# Leitura dos dados de teste:
df_teste = pd.read_csv("dados_base_analise_de_sentimentos/df_teste.csv")
# Definição de variáveis de teste e aplicação da função de tratamento de texto:
teste_x = tf_vectorizer.transform(df_teste['tweets'])
teste_y = df_teste['sentimento']
# Aplicação do modelo nos dados de teste:
predicao_MNB_teste = classificador_MNB.predict(teste_x)
resultado_MNB_teste = pd.Series(predicao_MNB_teste)
# Resultados da análise nos dados de teste:
resultado_MNB_teste.value_counts()
Avaliando a precisão do modelo criado:
# Matrix de confusão:
print (confusion_matrix(teste_y,resultado_MNB_teste))
# Verificação de métricas de precisão:
print (classification_report(teste_y,resultado_MNB_teste))
# Taxa de acurácia do modelo:
metrics.accuracy_score(teste_y,resultado_MNB_teste)
Nosso modelo apresentou uma taxa de 79% de acerto. Como nosso projeto faz análises de forma genérica e foi treinado com tweets também genéricos, essa taxa pode ser considerada um bom valor. Para melhora-lá pode-se treinar o modelo com tweets mais similares aos que ele irá analisar. Por exemplo, Se o objetivo é analisar tweets sobre cinema, treinar com tweets sobre cinema.
Vamos agora aplicar nosso modelo de predição de sentimentos nos tweets coletados.
# Aplicação da função de tratamento de texto:
analise_sent_tweets = tf_vectorizer.transform(df['text'])
# Aplicação do modelo
predicao_MNB = pd.Series(classificador_MNB.predict(analise_sent_tweets))
# Armazenamento dos resultados em uma nova variável chamada "analise_sentimento":
df['analise_sentimento'] = predicao_MNB
# Visualização do dataset após a análise de sentimentos:
df.head()
Foi mencionado anteriormente que também usaríamos o algoritmo Vader. Inicialmente ele foi descartado por trabalharmos com tweets em português. Porém durante as análises surgiu uma oportunidade para o utilizarmos e melhorar nossas predições.
O modelo Multinomial não trabalha com emojis, e durante o tratamento do texto eles são removidos. Entretanto, os emojis são cada vez mais usados e podem fazer total diferença no sentimento do conteúdo.
# frases com sentimentos Neutro, Positivo e Negativo
frases_para_teste = pd.Series(['Estou indo comprar um computador novo.',
'Estou indo comprar um computador novo. 😃',
'Estou indo comprar um computador novo. 😠'])
# Análise com modelo multinomialNB
resultado_teste_MNB = pd.Series(classificador_MNB.predict(tf_vectorizer.transform(frases_para_teste)))
resultado_teste_MNB
Podemos considerar que nosso modelo fez uma boa classificação, apesar da primeira frase possuir uma interpretação dúbia. Porém na última o emoji dar claramente uma conotação negativa a frase, e ela acaba não sendo captada.
O algoritmo Vader trabalha muito bem com emojis como veremos a seguir. Note que a primeira ele irá classificar como neutro por estar em um idioma em que ele não foi treinado.
# Criação do modelo Vader:
analyser = SentimentIntensityAnalyzer()
def vader(text):
score = analyser.polarity_scores(text)
lb = score['compound']
if lb >= 0.05:
return 'Positivo'
elif (lb > -0.05) and (lb < 0.05):
return 'Neutro'
else:
return 'Negativo'
resultado_teste_Vader = frases_para_teste.apply(lambda x: vader(x))
resultado_teste_Vader
Outra amostra de como o Vader consegue fazer cálculos com emojis e determinar se eles são positivos ou não:
# Novo teste com Vader:
frases_para_teste_2 = pd.Series(['😄', '👍 👎', '😐 😜 😁', '🧐 😠'])
resultado_teste_Vader = frases_para_teste_2.apply(lambda x: vader(x))
resultado_teste_Vader
Como os emojis tem sido cada vez mais usados, não podemos deixar de incluí-los em nossa análise. Por isso, vamos verificar quais são os tweets com emojis e extraí-los para uma nova variável, e classificá-los com o modelo Vader.
Como os emojis possuem um peso maior em relação a sentimentos do que as palavras, aqueles tweets em que o resultado do Vader for positivo ou negativo, a classificação será a do modelo Vader.
Nos casos onde os tweets não tenha emojis, ou tenha e o resultado do Vader seja neutro, será mantida a classificação feita pelo modelo MultinomialNB.

# Análise de emojis:
# Função de extração:
def extracao_de_emojis(str):
return ' '.join(c for c in str if c in emoji.UNICODE_EMOJI)
# Criação da nova variável contendo os emojis:
df['emojis_extraidos'] = df['text'].apply(lambda x: extracao_de_emojis(x))
# Aplicação do modelo Vader
df['analise_sent_emoji'] = df['emojis_extraidos'].apply(lambda x: vader(x))
# Substituição das análises dos tweets com emojis
df.loc[df['analise_sent_emoji'] == 'Positivo', 'analise_sentimento'] = 'Positivo'
df.loc[df['analise_sent_emoji'] == 'Negativo', 'analise_sentimento'] = 'Negativo'
# Visualização do dataset após a análise de sentimentos:
df.head()
Com nossa análise de sentimentos devidamente concluída podemos agora quantificá-la e produzir gráficos.
# Contagem de sentimentos
polaridade_sentimentos = pd.DataFrame(df['analise_sentimento'].value_counts()).reset_index()
polaridade_sentimentos.columns = ['sentimento', 'num_de_tweets']
polaridade_sentimentos
# Gráfico com a contagem de polaridade
cores_dic = {'Positivo': 'gold', 'Neutro': 'white', 'Negativo': 'darkgoldenrod'}
cores= polaridade_sentimentos['sentimento'].map(cores_dic)
fig = go.Figure(data = [go.Bar( x = polaridade_sentimentos['num_de_tweets'],
y = polaridade_sentimentos['sentimento'], orientation = 'h',
marker_color = cores)
]).update_yaxes(categoryorder = "total ascending")
fig.update_layout(template = "plotly_dark",
title = go.layout.Title(text = "Contagem de polaridade",xref = "paper", x = 0.5))
fig.show()
# Contagem de sentimentos por horário:
# Criação de um dataframe para essa análise:
total_sents = pd.DataFrame()
total_sents['horario'] = df['data_hora']
total_sents['sentimento'] = df['analise_sentimento']
# Contagem dos tweets positivos:
total_sents_pos = total_sents[total_sents['sentimento'] == 'Positivo']
total_sents_pos = total_sents_pos['horario'].value_counts().to_frame().reset_index()
total_sents_pos.columns = ['data_hora', 'qtd_tweets']
total_sents_pos = total_sents_pos.sort_values(by=['data_hora'])
# Contagem dos tweets neutros:
total_sents_neu = total_sents[total_sents['sentimento'] == 'Neutro']
total_sents_neu = total_sents_neu['horario'].value_counts().to_frame().reset_index()
total_sents_neu.columns = ['data_hora', 'qtd_tweets']
total_sents_neu = total_sents_neu.sort_values(by=['data_hora'])
# Contagem dos tweets negativos:
total_sents_neg = total_sents[total_sents['sentimento'] == 'Negativo']
total_sents_neg = total_sents_neg['horario'].value_counts().to_frame().reset_index()
total_sents_neg.columns = ['data_hora', 'qtd_tweets']
total_sents_neg = total_sents_neg.sort_values(by=['data_hora'])
# Gráfico com a contagem de sentimentos por horário:
fig = go.Figure()
fig.add_trace(go.Scatter(x=total_sents_pos['data_hora'], y=total_sents_pos['qtd_tweets'], line=dict(color='gold', width=1),name="Positivo"))
fig.add_trace(go.Scatter(x=total_sents_neu['data_hora'], y=total_sents_neu['qtd_tweets'], line=dict(color='white', width=1),name="Neutro"))
fig.add_trace(go.Scatter(x=total_sents_neg['data_hora'], y=total_sents_neg['qtd_tweets'], line=dict(color='darkgoldenrod', width=1),name="Negativo"))
fig.update_layout(template = "plotly_dark",
title = go.layout.Title(text = "Contagem de sentimentos por horário",xref = "paper", x = 0.5))
fig.show()
# Gráfico com a contagem de sentimentos por horário detalhado:
fig = make_subplots(
rows=3, cols=1, start_cell="top-left",
specs=[[{}],[{}],[{}]],
subplot_titles = ("Positivo", "Neutro", "Negativo"))
#fig = go.Figure()
# Positivo:
fig.add_trace(go.Scatter(x=total_sents_pos['data_hora'], y=total_sents_pos['qtd_tweets'], line=dict(color='gold', width=1),name="Positivo"), row = 1,col = 1)
# Neutro:
fig.add_trace(go.Scatter(x=total_sents_neu['data_hora'], y=total_sents_neu['qtd_tweets'], line=dict(color='white', width=1),name="Neutro"), row = 2 ,col = 1)
# Negativo:
fig.add_trace(go.Scatter(x=total_sents_neg['data_hora'], y=total_sents_neg['qtd_tweets'], line=dict(color='darkgoldenrod', width=1),name="Negativo"), row = 3,col = 1)
fig.update_layout(template = "plotly_dark",
title = go.layout.Title(text = "Contagem de sentimentos por horário",xref = "paper", x = 0.5))
fig.show()
Esta análise é em teoria similar à análise de retweets, mas com a diferença de que os "RT's" sempre estão presentes no início do conteúdo, já as hashtags podem estar em qualquer posição dentro da variável "text". Por isso requer alguns tratamentos adicionais, que também servirão para adiantar o trabalho na questão de top 5 palavras.
# Criação do dataframe para a análise:
verifica_hash = pd.DataFrame()
verifica_hash['text'] = df['text']
# Substituição dos caracteres # e @ antes do tratamento para que não sejam excluídos no processo:
verifica_hash['text'] = verifica_hash['text'].str.replace('#', 'hashtag_vl', regex=True)
verifica_hash['text'] = verifica_hash['text'].str.replace('@', 'arroba_vl', regex=True)
# Utilização do método CountVectorizer para criar uma matriz de documentos:
cv = CountVectorizer(strip_accents = None)
count_matrix = cv.fit_transform(verifica_hash['text'])
# Criação de um dataframe com o número de ocorrências das principais palavras em nosso dataset:
contagem_palavras = pd.DataFrame(cv.get_feature_names(), columns=["palavra"])
contagem_palavras["count"] = count_matrix.sum(axis=0).tolist()[0]
contagem_palavras = contagem_palavras.sort_values("count", ascending=False).reset_index(drop=True)
# Retorno do caracteres # e @:
contagem_palavras['palavra'] = contagem_palavras['palavra'].str.replace('hashtag_vl','#', regex=True)
contagem_palavras['palavra'] = contagem_palavras['palavra'].str.replace('arroba_vl','@', regex=True)
# Contagem de Hashtags:
hashtags = contagem_palavras[contagem_palavras['palavra'].str.startswith('#')]
# Separação e exibição das 5 mais presentes:
top5_hashtags = hashtags.head()
top5_hashtags
# Gráfico de contagem de hashtags:
fig = go.Figure(data = [go.Bar(y = top5_hashtags['count'], x = top5_hashtags['palavra'],
marker_color='gold')])
fig.update_layout(template = "plotly_dark",
title = go.layout.Title(text = "Top 5 hashtags #",xref = "paper", x = 0.5))
fig.show()
Nesse tópico, vamos aproveitar o dataframe "contagem_palavras" criado durante a etapa anterior e refiná-lo com alguns tratamentos que incluem: remoção de hashtags, nomes de usuários, termos irrelevantes e stop words.
# Arrobas de usuários citadas nos tweets:
arrobas_citadas = contagem_palavras[contagem_palavras['palavra'].str.startswith('@')]
# Remoção hashtags e arrobas
contagem_palavras_relevantes = contagem_palavras[(contagem_palavras['palavra'].isin(hashtags['palavra'])==False)]
contagem_palavras_relevantes = contagem_palavras_relevantes[(contagem_palavras_relevantes['palavra'].isin(arrobas_citadas['palavra'])==False)]
# Remoção de termos irrelevantes:
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != 'rt']
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != 'https']
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != 'http']
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != 'co']
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != '#']
# Remoção de stop words:
contagem_palavras_relevantes = contagem_palavras_relevantes[(contagem_palavras_relevantes['palavra'].isin(portugues_stops)==False)]
# Separação e exibição das 5 mais presentes:
top5_contagem_palavras_relevantes = contagem_palavras_relevantes.head()
top5_contagem_palavras_relevantes
# Gráfico de contagem de palavras relevantes:
fig = go.Figure(data = [go.Bar(x = top5_contagem_palavras_relevantes['palavra'],
y = top5_contagem_palavras_relevantes['count'],
marker_color='gold')])
fig.update_layout(template = "plotly_dark",
title = go.layout.Title(text = "Top 5 palavras",xref = "paper", x = 0.5))
fig.show()
A localização de um tweet pode ser obtida por duas variáveis a "location" e a "place". Com base em coletas anteriores, foi possível verificar que a "place" apresenta geralmente valor nulo. Isso porque é uma variável que só é preenchida quando o usuário opta por compartilhar sua localização exata no tweet, como em uma postagem de "check-in" em um restaurante, por exemplo. Esse recurso é pouco utilizado pelos usuários da rede, por isso nem coletamos esse dado.
Já a "location" é uma informação que consta na bio do usuário, não é precisa como a place, mas nos dá uma localização a nivel de cidade/estado/país. Entretanto, essa variável também tem seus problemas. Ela é um campo de texto livre, sem formato definido. Sendo assim os usuários escrevem de forma variada, só o país, país e cidade, estado e cidade, lugares fictícios, trocadilhos e outras coisas. Como não é um campo obrigatório, também temos o problema de falta de preenchimento, mas esse em um número bem menor.
Porém entre os que preenchem de forma minimamente adequada, é possivel notar um certo padrão. Veja alguns exemplos de preenchimento:
Geralmente temos presentes nomes de cidades e estados. Então conseguimos garantir uma certa precisão em relação ao estado, visto que com o nome da cidade é possível saber o estado, já o inverso não. Para essa análise utilizaremos um dataset disponibilizado no Kaggle que contém uma lista com todas as cidades brasileiras. link do dataset:https://www.kaggle.com/gilbertotrindade/cidades-brasileiras.
Assim como foi feito para análise de sentimentos, utilizei este dataset para criar um dicionário de dados em python (arquivo dicionario_brasil.py, disponível no projeto). Nele temos as cidades e estados e suas respectivas siglas (UF). A missão é mapear todas as localizações e substituí-las pelas siglas dos estados.
Geralmente ao final da análise é possível ter a localização estadual de um número signifcativos de tweets. Mas de acordo com os dados coletados esse número pode variar a ponto de não gerar informação relevante quando comparada ao todo.
# Leitura do arquivo que contém as cidades e estados brasileiros.
import dicionario_brasil
## Tratamento dos dados:
df['localizacao'] = df['location']
df['localizacao'] = df['localizacao'].str.upper()
df['localizacao'] = df['localizacao'].fillna('NULO')
df['localizacao'] = df['localizacao'].apply(unidecode)
df['localizacao'] = df['localizacao'].str.replace('BRASIL', 'NULO', regex=True)
df['localizacao'] = df['localizacao'].str.replace('-', ',', regex=True)
df['localizacao'] = df['localizacao'].str.replace('/', ',', regex=True)
df['localizacao'] = df['localizacao'].str.replace('|', ',', regex=True)
df['localizacao'] = df['localizacao'].str.replace(' ', '', regex=True)
# Divisão dos dois primeiros termos presentes:
df['loc_01'] = df['localizacao'].str.split(',').str[0]
df['loc_02'] = df['localizacao'].str.split(',').str[1]
# Mapeamento com dicionário para a criação da variável "estado":
df['estado'] = df['loc_01'].map(dicionario_brasil.dic)
df['estado_02'] = df['loc_02'].map(dicionario_brasil.dic)
df['estado'] = df['estado'].fillna(df['estado_02'])
# Verificação da quantidade de localizações estaduais obtidas:
tweets_localizados = sum(df['estado'].value_counts())
pct_tweets_localizados = round((tweets_localizados*100)/len(df),2)
# Visualização dos resultados:
print("Tweets localizados: ", tweets_localizados)
print("Percentual de tweets localizados: ", pct_tweets_localizados,"%")
# Dataframe com a contagem dos estados:
tweets_estados = df['estado'].value_counts().to_frame().reset_index()
tweets_estados.columns = ['estado', 'qtd_tweets']
# Separação e exibição dos 5 mais presentes:
top5_estados = tweets_estados.head()
top5_estados
# Gráfico de contagem de palavras relevantes:
fig = go.Figure(data = [go.Bar(x = top5_estados['estado'],
y = top5_estados['qtd_tweets'], marker_color = 'gold')])
fig.update_layout(template = "plotly_dark",
title = go.layout.Title(text = "Top 5 estados",xref = "paper", x = 0.5))
fig.show()
Para concluir nossa análise exploratória vamos encontrar algumas valores a respeito do nosso período de coleta:
# Calculando o tempo de coleta:
# Criação de um dataframe para a análise:
data_hora_coleta = pd.DataFrame()
# Separação de data e hora:
data_hora_coleta['data'] = [d.date() for d in df['data_hora']]
data_hora_coleta['hora'] = [d.time() for d in df['data_hora']]
# Coletando o último e o primeiro horário:
fim = data_hora_coleta['hora'].iloc[-1]
inicio = data_hora_coleta['hora'].iloc[0]
# Transformação dos dados:
horario_fim = datetime.datetime.combine(datetime.date.today(), fim)
horario_inicio = datetime.datetime.combine(datetime.date.today(), inicio)
# Cálculo do período de coleta:
periodo = horario_fim - horario_inicio
# Visualização do resultado:
print("Período de coleta: ", periodo)
# Taxa média de tweets por usuário:
if qtd_usuarios_unicos > 0:
media_tweets_por_usuario = round((total_tweets/ qtd_usuarios_unicos),2)
else:
media_tweets_por_usuario = 0
# Visualização do resultado:
print("Taxa média de tweets por usuário: ", media_tweets_por_usuario)
# Taxa média de tweets por usuário verificado:
if qtd_usuarios_verificados > 0:
media_tweets_por_usuario_verificado = round((total_tt_verificados/ qtd_usuarios_verificados),2)
else:
media_tweets_por_usuario_verificado = 0
# Visualização do resultado:
print("Taxa média de tweets por usuário verificado: ", media_tweets_por_usuario_verificado)
# Taxa média de tweets por usuário não verificado:
if qtd_usuarios_nao_verificados > 0:
media_tweets_por_usuario_nao_verificado = round((total_tt_nao_verificados/ qtd_usuarios_nao_verificados),2)
else:
media_tweets_por_usuario_nao_verificado = 0
# Visualização do resultado:
print("Taxa média de tweets por usuário verificado: ", media_tweets_por_usuario_nao_verificado)
# Taxa média de tweets por minuto:
tweets_por_minuto = round((total_tweets/(periodo.seconds/60)),2)
# Visualização do resultado:
print("Taxa média de tweets por minuto: ", tweets_por_minuto)
# Taxa média de tweets por segundo:
tweets_por_segundo = round(tweets_por_minuto/60,2)
# Visualização do resultado:
print("Taxa média de tweets por segundo: ", tweets_por_segundo)
# Visualização de tabela de taxas com plotly
values_tx = [['','','','Tweets','Por usuário', 'Por usuário verificado',
'Por usuário não verificado', 'Por minuto','Por segundo'],
['','','','Taxa média',media_tweets_por_usuario, media_tweets_por_usuario_verificado,
media_tweets_por_usuario_nao_verificado, tweets_por_minuto, tweets_por_segundo ]]
palavras_chaves = str(keywords).strip('[]')
fig = go.Figure(data=[go.Table(
columnorder = [1,2],
columnwidth = [5,5],
header = dict(values = [['Tweets'],['Taxa média']], line_color = 'rgb(122, 94, 8)', fill_color = 'black',
align = ['center','center'], font = dict(color = 'gold', size = 11), height = 25),
cells = dict(values = values_tx, line_color = [['rgb(122, 94, 8)'if (val != '') else 'black' for val in values_tx[0]],
['rgb(122, 94, 8)'if (val != '') else 'black' for val in values_tx[1]]],fill_color = 'black',
align = ['center', 'center'], font = dict(color = [['gold'if (val == 'Tweets') else
'white' for val in values_tx[0]], ['gold'if (val == 'Taxa média') else
'white' for val in values_tx[1]]], size = 11), height = 25))])
fig.add_trace(go.Table(header=dict(values=['Termos monitorados'],line_color='rgb(122, 94, 8)',fill_color='black',
align=['center','center'],font=dict(color='gold', size=12),height=30),
cells=dict(values=[[palavras_chaves,]],line_color='rgb(122, 94, 8)',
fill=dict(color=['black', 'black']), align=['center', 'center'],font_size=12,height=30)))
fig.update_layout(template = "plotly_dark",
title = go.layout.Title(text = "Taxas de tweets",xref = "paper", x = 0.5))
fig.show()
Por fim, vamos condensar toda as análises feitas em um dashboard que nos permita ver as principais informações em uma única página. Podemos usar o próprio jupyter notebook para prototipar nosso dashboard antes de fazê-lo em um arquivo .py, basta fazer alguns ajustes nos códigos utilizados para fazer os gráficos durante a análise exploratória.
# Dashboard
# Estrutura:
fig = make_subplots(
rows=3, cols=4, start_cell="top-left",
specs=[[{"colspan": 2},None, {"type": "domain"}, {"type": "domain"}],
[{"colspan": 2},None, {},{"rowspan": 2, "type": "table"}], [{},{},{},None]],
subplot_titles=("Tweets ao longo do período", total_tt_st, total_us_st, "Sentimento ao longo do período",
"Polaridade","","Top 5 hashtags", "Top 5 palavras", "Top 5 estados"))
# Tweets ao longo do período:
fig.add_trace(go.Scatter(x=tw_x_pd['data_hora'], y=tw_x_pd['qtd_tweets'], fill='tozeroy', mode= 'lines',
line=dict(color='gold', width=1),name=""),row=1, col=1)
# Quantidade de tweets:
fig.add_trace(go.Pie(labels = ['Originais', 'Retweets'], values = [originais, retweets], hole = .7,
marker_colors=['gold', 'darkgoldenrod'], textinfo='label+percent',
hoverinfo='value'),row=1, col=3)
# Quantidade de usúarios:
fig.add_trace(go.Pie(labels = ['Verificados', 'Não verificados'],
values = [qtd_usuarios_verificados, qtd_usuarios_nao_verificados],
hole = .5, marker_colors=['darkgoldenrod','gold'], textinfo='label+percent',
hoverinfo='value',rotation=90),row=1, col=4)
# Sentimento ao longo do período:
fig.add_trace(go.Scatter(x=total_sents_pos['data_hora'], y=total_sents_pos['qtd_tweets'], line=dict(color='gold', width=1),name="Positivo"),row=2, col=1)
fig.add_trace(go.Scatter(x=total_sents_neu['data_hora'], y=total_sents_neu['qtd_tweets'], line=dict(color='white', width=1),name="Neutro"),row=2, col=1)
fig.add_trace(go.Scatter(x=total_sents_neg['data_hora'], y=total_sents_neg['qtd_tweets'], line=dict(color='darkgoldenrod', width=1),name="Negativo"),row=2, col=1)
# Polaridade:
cores_dic = {'Positivo': 'gold', 'Neutro': 'white', 'Negativo': 'darkgoldenrod'}
cores= polaridade_sentimentos['sentimento'].map(cores_dic)
fig.add_trace(go.Bar( x=polaridade_sentimentos['num_de_tweets'],y=polaridade_sentimentos['sentimento'],orientation='h',
marker_color = cores, name=""),row=2, col=3).update_yaxes(categoryorder="total ascending")
# Tabela de taxas:
values_tx = [['','','','Tweets','Por usuário', 'Por usuário verificado','Por usuário não verificado',
'Por minuto', 'Por segundo'],
['','','','Taxa média',media_tweets_por_usuario, media_tweets_por_usuario_verificado,
media_tweets_por_usuario_nao_verificado, tweets_por_minuto, tweets_por_segundo ]]
palavras_chaves = str(keywords).strip('[]')
fig.add_trace(go.Table(columnorder = [1,2],columnwidth = [5,5],
header = dict( values = [['Tweets'],['Taxa média']],line_color='rgb(122, 94, 8)', fill_color='black',
align=['center','center'], font=dict(color='gold', size=11),height=25),
cells=dict(values=values_tx,
line_color=[['rgb(122, 94, 8)'if (val != '') else 'rgb(17, 17, 17)' for val in values_tx[0]],
['rgb(122, 94, 8)'if (val != '') else 'rgb(17, 17, 17)' for val in values_tx[1]]],
fill_color='rgb(17, 17, 17)',align=['center', 'center'],
font = dict(color =[['gold'if (val == 'Tweets') else 'white' for val in values_tx[0]],
['gold'if (val == 'Taxa média') else 'white' for val in values_tx[1]]], size = 11),
height=25)),row=2, col=4)
fig.add_trace(go.Table(header=dict(values=['Termos monitorados'],line_color='rgb(122, 94, 8)',fill_color='rgb(17, 17, 17)',
align=['center','center'],font=dict(color='gold', size=12),height=30),
cells=dict(values=[[palavras_chaves,]],line_color='rgb(122, 94, 8)',
fill=dict(color=['rgb(17, 17, 17)', 'rgb(17, 17, 17)']),
align=['center', 'center'],font_size=12,height=30)),row=2, col=4)
# Top 5 Hashtags:
fig.add_trace(go.Bar(y = top5_hashtags['count'], x = top5_hashtags['palavra'],
marker_color='gold',name=""),row=3, col=1)
# Top 5 Palavras:
fig.add_trace(go.Bar(y = top5_contagem_palavras_relevantes['palavra'],
x = top5_contagem_palavras_relevantes['count'],
marker_color='darkgoldenrod',orientation='h',name=""),row=3, col=2)
# Top 5 Estados:
fig.add_trace(go.Bar(x = top5_estados['estado'].head(5), y=top5_estados['qtd_tweets'].head(5),
marker_color='gold',name=""),row=3, col=3)
# Configura tema:
fig.update_layout(showlegend=False, title=go.layout.Title(text="Bem-te-vi Dashboard",xref="paper",x=0.5),
template="plotly_dark").update_xaxes(showgrid=False).update_yaxes(showgrid=False)
# Exibe dashboard no notebook:
fig.show()
# Cria um arquivo html com o dashboard:
fig.write_html('passo-a-passo_dashboard.html', auto_open=True)
Esse mesmo código do dashboard e todos os outros de cálculos e análises são usados(adaptados) no arquivo dashboard.py para gerar o nosso dashboard de monitoramento.
Pode-se também salvar em formato .csv o dataset atualizado com os dados obtidos nas análises.
Note que excluiremos as colunas criadas para manipulação dos dados, mantendo apenas as originais e as com os valores finais das análises:
# Criação do dataset atualizado com a análise.
dataset_final = df.drop(['estado_02','emojis_extraidos','analise_sent_emoji',
'localizacao', 'loc_01', 'loc_02'],axis=1)
# Exporta o dataset com as análises em arquivo .csv
# dataset_final.to_csv('dataset_final.csv', index=False)
Com os arquivos executa_todos.py e gera_relatorio.sh, fazemos todas essas etapas de forma automatizada sem necessidade de abrir o jupyter notebook.
O executa_todos.py inicia a coleta e abre um dashboard de monitoramento que é atualizado periodicamente.
Já o gera_relatorio.sh gera um relatório com os gráficos obtidos nas análises, além de um dataset em .csv e um arquivo com o último estado do dashboard.